
//Copyright 1997-2009 Syrinx Development, Inc.
//This file is part of the Syrinx Web Application Framework (SWAF).
// == BEGIN LICENSE ==
//
// Licensed under the terms of any of the following licenses at your
// choice:
//
//  - GNU General Public License Version 3 or later (the "GPL")
//    http://www.gnu.org/licenses/gpl.html
//
//  - GNU Lesser General Public License Version 3 or later (the "LGPL")
//    http://www.gnu.org/licenses/lgpl.html
//
//  - Mozilla Public License Version 1.1 or later (the "MPL")
//    http://www.mozilla.org/MPL/MPL-1.1.html
//
// == END LICENSE ==
using System;
using System.Collections;
using System.CodeDom;
using System.CodeDom.Compiler;
using System.Diagnostics;
using System.IO;
using System.Text;
using Microsoft.CSharp;
 
using Swaf;
using Swaf.BizObj;
using Swaf.Container;

namespace Swaf.BizObj.EarlyBinding
{
	/// <summary>
	/// This class will inspect a CSF application configuration and generate early bound
	/// classes for each business object defined within it.
	/// </summary>
	/// <remarks>
	/// 
	/// </remarks>
	public class EarlyBindBizObjGenerator
	{
		private static string[] s_fileNames = {
			"classTemplate.txt","methodTemplate.txt", "propertyTemplate.txt",
			"interfaceTemplate.txt","methodDefTemplate.txt", "propertyDefTemplate.txt",
			"ListTemplate.txt",
			"featureTemplate.txt", "classFactoryEntryTemplate.txt"
		};

		private static string[] s_assemblyAssemblyReferences =
			{"SwafInterfaces.dll", "Swaf.dll", "System.dll", "System.XML.dll"};
			
												
		private IApplication m_app;
		private IApplication m_isolatedApp;
		private string[] m_templates = new string[s_fileNames.Length];
		private ArrayList m_files = new ArrayList();

		private ArrayList m_exceptions = new ArrayList();

		private IDictionary m_typeNameMap = new Hashtable();

		public EarlyBindBizObjGenerator()
		{
			setupTemplates();
		}

		/// <summary>
		/// Given an well formed CSF application configuration and the various options
		/// this method will examine the business object definitions in the application
		/// configuration and create an early bound interface, business object class and business
		/// object list class for each business object defined in the application.
		/// </summary>
		/// <remarks>
		/// This method starts an isolated application of the CSF application provided in a 
		/// "browse mode" that allows it to inspect the application and get the business object
		/// definitions out of it.
		/// </remarks>
		/// <param name="baseConfig">The name of the application configuration xml file</param>
		/// <param name="baseLoc">The base location passed to CSF application when it is started</param>
		/// <param name="binLoc">The base location to run the application in</param>
		/// <param name="outputLocation">The location to place the generated code, feature definition and 
		/// assembly</param>
		/// <param name="compileOutput">If true, this method will attempt to compile the code into
		/// a library assembly.  If false, this method will only generate the feature definition and
		/// code for the business objects.</param>
		/// <param name="secondPassCompile">If true, this code will work hard to create an assembly even 
		/// if it has to remove some business object definitions that failed so that a sub-set of business
		/// objects have their classes.</param>
		/// <param name="generateDebugInfo">If true, when creating the assembly it will include debug info
		/// else it will be a release build.</param>
		/// <returns></returns>
		public ArrayList generateFromConfig(string baseConfig, string baseLoc, string binLoc, string outputLocation,
			bool compileOutput, bool secondPassCompile, bool generateDebugInfo)
		{
			StringBuilder buff = new StringBuilder();
			IsolatedApplication isoApp = null;
			try
			{
				isoApp = new IsolatedApplication("", baseConfig, binLoc, baseLoc, baseLoc, true);
			}
			catch(Exception e)
			{
				m_exceptions.Add(new BizObjProcessingException("BizObj App isolated startup", "Cannot start", e));
				return m_exceptions; //If we cannot start the isolated app it is catastrophic.
			}
			m_isolatedApp = isoApp.App;
			IBizObjManager mgr = isoApp.App.bizObjMgr;
			IDictionary keys = new FlexiMap();
			string appName = m_isolatedApp.resMgr.getMacro("ApplicationId").Replace(" ", "");
			foreach(string name in mgr.Definitions)
			{
				IBizObjDefinition def = null;
				try
				{
					def = mgr.getDefinition(name);
					if(def.Parent != null)
					{
						//Only generate if this is not the CSF BizObj business object, which
						//is the only bizobj that would not have a parent.
						generateInterface(outputLocation, def);
						generateClass(outputLocation, def);
						generateClassList(outputLocation, def);

						keys["FactoryName"] = def.Name;
						keys["ClassName"] = calcClassName(def.Name);
						buff.Append(m_isolatedApp.resMgr.resolve(m_templates[8], keys));

						keys["FactoryName"] = def.Name + "List";
						keys["ClassName"] = calcClassName(def.Name) + "List";
						buff.Append(m_isolatedApp.resMgr.resolve(m_templates[8], keys));
					}
				}
				catch(Exception e)
				{
					m_exceptions.Add(new BizObjProcessingException("Early bound bizobj code creation", "cannot create early bound interface for " + def.Name, e));
				}
			}

			try
			{
				StreamWriter fileOut = File.CreateText(Path.Combine(outputLocation,  appName + "EarlyBoundBizObjsFeature.xml"));
				keys["ClassFactoryEntries"] = buff.ToString();
				fileOut.Write(App.resMgr.resolve(m_templates[7], keys));
				fileOut.Close();
			}
			catch(Exception e)
			{
				m_exceptions.Add(new BizObjProcessingException("Feature definition to file", "Cannot create feature definition file", e));
			}

			CompilerResults compileRC = null;
			if(compileOutput)
			{
				try
				{
					compileRC = compile(outputLocation, secondPassCompile, generateDebugInfo);
				}
				catch(Exception e)
				{
					m_exceptions.Add(new BizObjProcessingException("Compile assembly" , "cannot compile code:", e));
				}
				if(compileRC != null && compileRC.Errors.HasErrors)
				{
					addCompilerErrors(compileRC);
				}
			}

			return m_exceptions;
		}

		protected void generateClass(string baseLoc, IBizObjDefinition def)
		{
			IDictionary keys = new FlexiMap();
			string defName = calcClassName(def.Name);

			keys["ClassName"] = defName;
			keys["ParentName"] = calcInterfaceName(def.Name);
			keys["ClassBody"] = createBody(def, m_templates[1], m_templates[2], false);
			string classStuff = App.resMgr.resolve(m_templates[0], keys);
			if(classStuff != null)
			{
				string fileName = Path.Combine(baseLoc, defName + ".cs");
				StreamWriter fileOut = File.CreateText(fileName);
				fileOut.Write(classStuff);
				fileOut.Close();
				m_files.Add(fileName);
			}
		}

		protected void generateInterface(string baseLoc, IBizObjDefinition def)
		{
			IDictionary keys = new FlexiMap();
			string defName = calcInterfaceName(def.Name);
			keys["InterfaceName"] = defName;
			if(def.Parent == null)
				keys["ParentInterfaceName"] = "IBizObj";
			else
				keys["ParentInterfaceName"] = calcInterfaceName(def.Parent.Name);
			keys["InterfaceBody"] = createBody(def, m_templates[4], m_templates[5], true);
			string interfaceStuff = App.resMgr.resolve(m_templates[3], keys);
			if(interfaceStuff != null)
			{
				string fileName = Path.Combine(baseLoc, defName + ".cs");
				StreamWriter fileOut = File.CreateText(Path.Combine(baseLoc, defName + ".cs"));
				fileOut.Write(interfaceStuff);
				fileOut.Close();
				m_files.Add(fileName);
			}
		}

		protected void generateClassList(string baseLoc, IBizObjDefinition def)
		{
			IDictionary keys = new FlexiMap();
			string defName = calcClassName(def.Name);

			keys["ClassName"] = defName;
			keys["BizObjName"] = def.Name;
			string classStuff = App.resMgr.resolve(m_templates[6], keys);
			if(classStuff != null)
			{
				string fileName = Path.Combine(baseLoc, defName + "List.cs");
				StreamWriter fileOut = File.CreateText(fileName);
				fileOut.Write(classStuff);
				fileOut.Close();
				m_files.Add(fileName);
			}
		}

		/// <summary>
		/// This method will create the inner contents of the interface or class based on the
		/// properties and methods of the business object definition given.
		/// </summary>
		/// <remarks>
		/// This class uses the passed templates to generate the content.  This allows callers
		/// to pick between the appropriate template and pass them in here.
		/// 
		/// NOTE: If a new type of business object field type is created, this method
		/// should be updated.  It only supports NetClass, BizObj, BizObjList, and 
		/// Method.  If a new entry is added to the BizFieldType enumeration this method
		/// should be revised.
		/// </remarks>
		/// <param name="def">The business object definition to build off of</param>
		/// <param name="methodTemplate">The template with resMgr macros that will generate
		/// the right code for a business object method.</param>
		/// <param name="propTemplate">The template with resMgr macros that will generate
		/// the right code for a business object property</param>
		/// <param name="ignoreInheritedFields">If true, this method will not generate code
		/// for properties or methods that are inherited from a parent business object.  This is
		/// good for interfaces because they do not want to relist their parents code elements.  
		/// However, because we dont use inheritance for classes, this method can product
		/// implementations for all the fields of the given definition.</param>
		/// <returns>The body of the class or interface that was created by this method</returns>
		protected string createBody(IBizObjDefinition def, string methodTemplate, string propTemplate, bool ignoreInheritedFields)
		{
			string defName = def.Name;
			IDictionary keys = new FlexiMap();
			StringBuilder buff = new StringBuilder();
			foreach(IBizObjField fld in def.FieldList)
			{
				if(!ignoreInheritedFields || !fld.InheritedField)
				{
					string fldName = fld.Name;
					if(fldName == "")
						fldName = "_default";
					if(fldName == cleanBizObjName(def.Name))
						fldName = "_" + fldName;
					switch(fld.BizType)
					{
						case BizFieldType.NetClass:
							if(fld.SampleValue != null)
								keys["PropertyType"] = fld.SampleValue.GetType().FullName;
							else
								keys["PropertyType"] = getTypeName(fld.FactoryType);
							keys["PropertyName"] = fldName;
							buff.Append(App.resMgr.resolve(propTemplate, keys));
							break;
						case BizFieldType.BizObj:
							keys["PropertyName"] = fldName;
							keys["PropertyType"] = calcInterfaceName(fld.getBizObjType());
							//keys["PropertyType"] = calcInterfaceName(((IBizObj)fld.SampleValue).type.Name);
							buff.Append(App.resMgr.resolve(propTemplate, keys));
							break;
						case BizFieldType.BizObjList:
							keys["PropertyName"] = fldName;
							keys["PropertyType"] = calcListName(fld.getBizObjType());
							//keys["PropertyType"] = calcListName(((IBizObjList)fld.SampleValue).objectType.Name);
							buff.Append(App.resMgr.resolve(propTemplate, keys));
							break;
						case BizFieldType.Method:
							keys["MethodName"] = fldName;
							buff.Append(App.resMgr.resolve(methodTemplate, keys));
							break;
					}
				}
			}
			return buff.ToString();
		}

		/// <summary>
		///When a bizobj field has a null default value, its type cannot be retrieved from the sample
		///value.  In those cases this method is called which will look in its m_typeNameMap to see if
		///it has used the given name before.  If it hasn't this method will create an instance out
		///of the bizobjfield class factory and get the full type name from it and then cache the
		///type name in the m_typeNameMap.
		/// </summary>
		/// <param name="name">The class factory nick name to calculate the real .net class type for</param>
		/// <returns></returns>
		protected string getTypeName(string name)
		{
			if(!m_typeNameMap.Contains(name))
				m_typeNameMap["name"] = m_isolatedApp.factories["BizObjBizFieldTypes"].createFromNickName(name).GetType().FullName;
			return (string)m_typeNameMap["name"];
		}

		protected string cleanBizObjName(string bizObjName)
		{
			return bizObjName.Replace("::", "");
		}

		protected string calcInterfaceName(string bizObjName)
		{
			return "I" + cleanBizObjName(bizObjName);
		}

		protected string calcClassName(string bizObjName)
		{
			return cleanBizObjName(bizObjName);
		}

		protected string calcListName(string bizObjName)
		{
			return calcClassName(bizObjName) + "List";
		}

		/// <summary>
		/// This method will read all the code templates used by this class from the CodeTemplates
		/// catalog directory and cache them in m_templates.  This is a performance method that
		/// allows the templates to be reused for each bizobj without having to read the template
		/// from the file each time.
		/// </summary>
		protected void setupTemplates()
		{
			string baseTemplateUrl = App.resMgr.resolve("$CatalogHome$/CodeTemplates/");
			int templateIndex = 0;
			foreach(string fileName in s_fileNames)
			{
				string fullName = Path.Combine(baseTemplateUrl, fileName);
				FileInfo f = new FileInfo(fullName);
				FileStream s = f.OpenRead();
				TextReader readIn = new StreamReader(s);
				m_templates[templateIndex++] = readIn.ReadToEnd();

				readIn.Close();
				s.Close();
			}
		}

		protected string calcCodeTemplate()
		{
			return "";
		}

		/// <summary>
		/// Given the compiler errors, this method will add the exceptions
		/// into the m_exceptions collection for the combined output of the
		/// work being done by this class.
		/// </summary>
		/// <param name="rc"></param>
		protected void addCompilerErrors(CompilerResults rc)
		{

			foreach(CompilerError compileErr in rc.Errors)
			{
				m_exceptions.Add(new BizObjProcessingException("Compile assembly",
					compileErr.ToString(), null));
			}
		}

		/// <summary>
		/// Given the file names in m_files, this will use System.CodeDom.Compiler to
		/// compile all the code files into a library assembly.
		/// </summary>
		/// <remarks>This method can call itself recursively one time to try and
		/// compile again with the files that cause exceptions the first time 
		/// removed from the main file list of generated code.</remarks>
		/// <param name="baseLoc">The file system location to use to find the code files</param>
		/// <param name="tryAgain">If true, the code will do a second pass compile if
		/// exceptions occured on the first pass.
		/// </param>
		/// <param name="generateDebugInfo">Will compile with debug info if true.</param>
		/// <returns>The exceptions that occured on the last compile done.  If second
		/// pass compile was done, the exceptions from the 2nd pass are returned.  Note
		/// that the exceptions from the first pass are still added to the main
		/// list of exceptions in this.</returns>
		protected CompilerResults compile(string baseLoc, bool tryAgain, bool generateDebugInfo)
		{
			CSharpCodeProvider provider = new CSharpCodeProvider();
			ICodeCompiler compiler = provider.CreateCompiler();
			string assemblyName = Path.Combine(baseLoc, m_isolatedApp.resMgr.getMacro("ApplicationId") + ".EarlyBoundBizObjs.dll");
			CompilerParameters p = new CompilerParameters(s_assemblyAssemblyReferences,	assemblyName, generateDebugInfo);

			string[] fileNames = (string[])m_files.ToArray(typeof(string));
			CompilerResults rc = compiler.CompileAssemblyFromFileBatch(p, fileNames);

			//Try for a second pass at compiling the bizobj dll by removing the files that
			//had compile errors and trying again.
			if(rc.Errors.HasErrors && tryAgain)
			{
				addCompilerErrors(rc);
				foreach(CompilerError e in rc.Errors)
					try
					{
						m_files.Remove(Path.Combine(baseLoc, e.FileName));
					}
					catch {	} //exception for removing a file more then once thrown out.
				rc = compile(baseLoc, false, generateDebugInfo);
			}
			return rc;
		}

		protected IApplication App
		{
			get
			{
				if(m_app == null)
					m_app = Application.globalApp;
				return m_app;
			}
		}
	}
}
